12. Deployment
During development, the application runs under the control of [ng serve]: compiled on the fly, with the checks and messages from [Angular] for the developer, and reloaded with every change. To deploy it, we build it: [ng build] produces an optimized web application, without any development tools. This is the one we deploy to a web server, and it is ALSO this one that [Capacitor] bundles into the [Android] application: there is only one build, for both platforms.
12.1. The web version
- Build the client. In the [rdvmedecins_ionic] folder:
The console displays (the list of files has been truncated):
Without any options, [ng build] uses the configuration [production] from [angular.json]: the code is optimized (names are shortened, spaces and comments are removed, and unused code is eliminated), and checks intended for development are removed. The compiler compiled the application ([TypeScript], templates, [Angular], [Ionic]) into [JavaScript], in the [dist/rdvmedecins_ionic/browser/] folder:
- [index.html]: THE application page, which loads the [JavaScript] files and the stylesheet. Unlike the [Flutter] client, the application generates standard HTML content, which the browser displays itself;
- the initial “chunks,” loaded at startup: [Angular], [Ionic], the router, the services, and the layout. 1.1 MB, or approximately 220 KB once compressed by the server: this is the estimate in the last column;
- "lazy" segments: one per screen ([login], [my-account]...), thanks to [loadComponent] for the routes (see [app.routes.ts]), and a few others for the components of [Ionic] loaded on demand. Each is downloaded only on the first visit to the screen;
- the name of each file contains a hash of its contents ([main-FA64HVK2.js]): the server can instruct the browser to cache them indefinitely, since a new version will have new names;
- [i18n/] and [favicon.ico]: the contents of the [public/] folder, copied as-is;
- [angular.json] sets “limits” for the [production] configuration: a warning if the initial chunks exceed 2 MB, and an error if they exceed 5 MB. The application is far from reaching these limits.
The [--define "SERVER='...'"] option is not needed here: served by the JSON server, the application uses the relative path of [config.ts] ([/api/...]).
-
Deploy it to the server. The JSON server can serve a compiled web client (the server’s [src/web/client-spa.ts] file): its files are looked for in the [client/] folder, at the root of the server, or in the folder specified by the [CLIENT_DIR] environment variable (server file [src/config/paths.ts]; it can be written to the file [.env]). Two options:
-
copy the CONTENTS of the [dist/rdvmedecins_ionic/browser/] folder to a [client/] folder at the root of the server JSON ([rdvmedecins-nestjs-json/client/index.html], [rdvmedecins-nestjs-json/client/main-FA64HVK2.js]...);
- or specify the folder generated by [ng build] directly:
(a path relative to the server root, or an absolute path). Then restart the server. It displays:
The complete application is now available at http://localhost:8080. The server serves the client’s files and returns [index.html] for any other address outside of /api: reloading the page /schedule?doctorId=1&day=2026-10-13 works (it is the client’s browser that displays the page for this address). The page and the API come from the same origin: no CORS, and the token cookie is being passed along.
To test this without stopping the development server, you can launch an instance of the JSON server on a different port, specifying both variables on the command line (here under [Linux] or [macOS]; on [Windows], with [PowerShell], you first define them as [$env:PORT=8090] and [$env:CLIENT_DIR='...']):
The application is then accessible at http://localhost:8090; the two instances share the same database. This is how the web version of this client was tested: login, calendar, reloading the page at an address like /schedule?..., and the translated tab title.
The client can also be deployed on any web server ([nginx], [Apache]...), provided that it forwards /api requests to the JSON server (a “reverse proxy”) and redirects to [index.html] for unknown URLs: this is what [ng serve] does during development.
12.2. The Android version
Building the [Android] application requires [Android Studio] and SDK from [Android] (see the “Setting Up the Development Environment” chapter). [Capacitor] 8 requires JDK 21 or later: the version included with [Android Studio] is suitable.
- Create the [Android] project once:
The command creates the [android/] folder: a complete [Android Studio] project (a [Gradle] project), which contains a [Android] activity displaying a [WebView], and the [Capacitor] plugins used by the application ([@capacitor/app], [@capacitor/preferences]). This file is part of the project: keep it (and save it with the source code in [git]), because this is where you set the application’s name, icon, and signature.
If JDK is a [Java] 25 (the one used in recent versions of [Android Studio]), the version of [Gradle] selected by [Capacitor] (8.14) cannot make it work ([Unsupported class file major version 69]): we switch to [Gradle] 9.1 in the [android/gradle/wrapper/gradle-wrapper.properties] file (see example 01):
- Build the web application, then copy it to the [Android] project:
- line 1: the web application, in [dist/rdvmedecins_ionic/browser/];
- Line 2: [cap sync] copies this folder ([webDir] from [capacitor.config.ts]) to [android/app/src/main/assets/public/], along with the configuration from [Capacitor] ([capacitor.config.json]), and updates the plugins. With [server.cleartext] to [true], it also adds the [android:usesCleartextTraffic="true"] attribute to the plugin manifest, which is merged with the application’s manifest during the build: [http://] requests (unencrypted) to the development server are allowed.
You must rerun these two commands after every change to the application code.
- Run it:
The command lists the available devices (emulators created in [Android Studio], phones connected via USB with “USB debugging” enabled), builds the application, installs it on the selected device, and launches it. You can also open the project in [Android Studio] ([npx cap open android]) and use its “Run” button.
On the emulator, the app connects to the server at the address [http://10.0.2.2:8080] (see [config.ts]). On a real phone connected to the same Wi-Fi network as the computer, you must provide it with the computer’s address on that network ([ipconfig] under [Windows]) using the following syntax:
The computer’s firewall must allow incoming connections on port 8080.
-
Test it. The application is a web page displayed by [WebView]: the development tools on [Chrome], on the computer, can inspect it. Open the address [chrome://inspect]: the emulator or phone application appears there; "Inspect" opens the console, the element inspector, and the "Network" tab (since requests to API go through [CapacitorHttp]—that is, through native code—they are less visible there than on the web: the console for [Logcat], within [Android Studio], displays them).
-
A debug installation file (APK). In [Android Studio]: “Build > Generate App Bundles” or “APKs > Generate APKs.” Or, from the command line, in the [android/] folder:
(under [Windows]: [gradlew assembleDebug]). The file is generated in [android/app/build/outputs/apk/debug/app-debug.apk]. It is signed with the debug key from [Android Studio]: this is sufficient for testing. Install it on a phone connected via [adb install app-debug.apk], or by copying the file to the phone and opening it ([Android] will then prompt you to allow the installation of apps “from unknown sources”).
-
A signed production version. A published app must be signed with a key specific to the developer, which must be kept secure (all updates must be signed with the same key):
-
in [Android Studio]: the “Build > Generate Signed App Bundle or APK...” menu, which also allows you to create the key (a “keystore” file, protected by a password);
- or via the command line: create the key using the [keytool] tool from JDK, then build the signed version:
- line 1: creates the key in the [rdvmedecins.jks] file (the command prompts for the developer’s name, organization, etc.);
- line 2: [cap build] initiates the build [Gradle] of the “release” version ([assembleRelease]), and signs the resulting file with this key (the key path is relative to the [android/] folder). [--signing-type apksigner]: the signature is created by the [apksigner] tool from SDK, whose folder, [build-tools/<version>], must be located within PATH; Without this option, [Capacitor] uses [jarsigner], whose signature is no longer accepted by recent versions of [Android] for a APK. [--androidreleasetype AAB] would produce an “Android App Bundle,” the format expected by [Google Play Store].
You can also describe the signature in the [android/app/build.gradle] file (a [signingConfigs] block; see the [Android] documentation titled “Sign your app”), and run [./gradlew assembleRelease] directly (a APK, located in [android/app/build/outputs/apk/release/]) or [./gradlew bundleRelease] (an App Bundle). The key password must not be written to a file stored with the source code.
-
For a real release:
-
the application name is set in [capacitor.config.ts] ([appName]) and in [android/app/src/main/res/values/strings.xml]; its icon is set in the [android/app/src/main/res/mipmap-*/] files (the [@capacitor/assets] tool generates all of them from a single image);
- the server must be in HTTPS (passwords are shared): we remove [server.cleartext] from [capacitor.config.ts], and build the application with the server’s public address ([--define "SERVER='https://rdvmedecins.example.com'"]).
12.3. The production server
In actual production, don’t forget the file [.env] on the server:
- a long, random [JWT_SECRET];
- an empty [CAPTCHA_TEST];
- [COOKIE_SECURE=true] if the site is served via HTTPS (it must be): the cookie is then only transmitted over encrypted connections;
- [TRUST_PROXY=true] if the server is located behind a proxy ([nginx]...), so that the attempt limit sees the clients’ actual IP address.
The server has no CORS rules to declare: on the web, the client is served by the server itself (same origin); on [Android], requests originate from the native code ([CapacitorHttp]), to which CORS does not apply.
We then launch the compiled server: [npm run build], followed by [npm run start:prod].