Prepare Oracle Cloud
First, the ground to build on: a network where the instance is reachable from the internet and the database stays out of sight.
1.1Create the network
In your compartment, create a VCN (Oracle's virtual private network) with two subnets: a public one for the instance and a private one for the database. That way the database has no address visible from the internet.
In the security list, allow ports 80 and 443 from the internet to the instance, and port 5432 from the public subnet to the private one. Port 80 is used to obtain HTTPS certificates, 443 serves the dashboard and the chargers, 5432 is PostgreSQL.
What you should see Your VCN has a public and a private subnet, and the security list shows the 80, 443 and 5432 rules.
1.2Pick the instance shape
For a few hundred chargers, the product guide suggests a VM.Standard.E5.Flex instance with 1 OCPU and 8 GB of memory. For a trial, a VM.Standard.A1.Flex (Arm Ampere processor) with 1 OCPU and 6 GB is enough, together with the local database described in the next chapter.
One OCPU is two virtual CPUs on x86 and one core on Ampere. Oracle's cost estimator gives you the exact price for your region.
Watch out On an Arm instance, don't run images built for amd64: they stop with "exec format error". If you follow this guide, docker compose builds the images on the instance itself, so in the right architecture.
Database and secrets
The database holds everything: chargers, sessions, users, payments. A managed service means you don't have to think about PostgreSQL backups and upgrades.
2.1Create the PostgreSQL database
Create an OCI Database with PostgreSQL, version 16 or later, 2 OCPU shape, in the private subnet. Turn on daily backups with at least 7 days of retention.
Then create the ev_supervision database and an ev_supervision user that owns it. This matters: on first start the server creates all the tables itself, so it has to own the database. Write down the database's private endpoint, you'll need it for the configuration.
For a quick trial you can skip the managed service: Docker Compose can run PostgreSQL on the instance, in a local volume. That's the local-db profile, covered in the Configure chapter.
What you should see The console shows the database as active, with a private address in your private subnet.
2.2Generate and store the secrets
Two secrets are essential: the database password and the APP_ENCRYPTION_KEY, which protects sensitive data in the database. Generate each one with the command below, run twice.
Store them in an OCI Vault, with a master key, as database-password and app-encryption-key. If the instance should read these secrets itself, put it in a dynamic group allowed to read secret-bundles in the compartment.
Terminal openssl rand -base64 32What you should see A 44-character string appears each time you run it.
Watch out Without APP_ENCRYPTION_KEY, the encrypted data in the database can no longer be read. Keep an offline copy, separate from the database backups.
Create the instance and get the code
Now for the machine that will run the server, the dashboard and Caddy.
3.1Launch the instance
From your computer, launch the instance in the public subnet, with a public IP and your SSH key. Replace <compartment>, <availability-domain>, <Ubuntu 24.04 image> and <public-subnet> with the OCIDs from your account. You can do exactly the same in the web console.
Terminal oci compute instance launch --compartment-id <compartment> \ --availability-domain <availability-domain> --shape VM.Standard.E5.Flex \ --shape-config '{"ocpus":1,"memoryInGBs":8}' --image-id <Ubuntu 24.04 image> \ --subnet-id <public-subnet> --assign-public-ip true \ --display-name ev-supervision --ssh-authorized-keys-file ~/.ssh/id_ed25519.pubWhat you should see The ev-supervision instance reaches the running state and shows a public IP address. You can SSH into it.
3.2Open the system firewall
OCI's Ubuntu and Oracle Linux images filter incoming ports inside the operating system too, on top of the security list. On Ubuntu, open 80 and 443 in /etc/iptables/rules.v4. On Oracle Linux, use firewall-cmd.
Watch out This is the most common trap on Oracle: the security list allows 80 and 443, but the system keeps them closed. HTTPS certificates can't be issued and chargers can't connect.
3.3Install Docker and get the code
On the instance, install Docker Engine with the Compose plugin, following Docker's official instructions for Ubuntu.
Before cloning, read the Codetics attribution licence in the repository: cloning it means you accept that licence. Then clone it and move into the deploy folder, where you'll run every command that follows.
Terminal git clone https://github.com/codetics-software/ev-server-lite ev-supervision && cd ev-supervision/deployWhat you should see The deploy folder contains docker-compose.yml, .env.example, server.env.example and the caddy folder, among others.
Configure the installation
The whole configuration fits in two files: .env, read by Docker Compose, and server.env, passed to the server. Everything else (internal addresses, trusted proxies, port 8080) is already set in docker-compose.yml, so you can leave it alone.
4.1Point your domain at the instance
With your DNS provider, create a record pointing your name, for example charging.your-domain.com, at the instance's public IP. Do it before the first start: Caddy needs it to get the Let's Encrypt certificate.
What you should see ping or dig on your name returns the instance's public IP.
4.2Copy the example files
Both examples are your starting point, with comments in French.
Terminal cp .env.example .env cp server.env.example server.env4.3Fill in .env
Put your domain name in APP_HOST, and in ACME_EMAIL the address that should receive Let's Encrypt notices. Empty COMPOSE_PROFILES to use the managed database, then set DATABASE_URL with the private endpoint you wrote down and DATABASE_PASSWORD with the database-password secret.
For a trial with the local database, keep COMPOSE_PROFILES=local-db and leave DATABASE_URL unset. The OCPP_HOST line is only for chargers on security profile 3: leave it empty.
deploy/.env APP_HOST=<your-domain> OCPP_HOST= ACME_EMAIL=<operations-email> COMPOSE_PROFILES= DATABASE_URL=jdbc:postgresql://<private endpoint>:5432/ev_supervision?sslmode=require DATABASE_USER=ev_supervision DATABASE_PASSWORD=<database-password secret> JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError4.4Fill in server.env
The first admin account is created on first start, and only if the database has no users yet. BOOTSTRAP_TENANT is your workspace ID, which you'll type every time you sign in, so replace codetics with your own now. Pick a long passphrase for BOOTSTRAP_PASSWORD.
Paste the app-encryption-key into APP_ENCRYPTION_KEY. OCPP_PUBLIC_URL is the address shown on each charger's commissioning sheet: wss:// followed by your domain and /ocpp. Leave the Stripe and email blocks empty for now, we'll get to them.
deploy/server.env BOOTSTRAP_TENANT=<workspace-id> BOOTSTRAP_EMAIL=<admin-email> BOOTSTRAP_PASSWORD=<long-passphrase> APP_ENCRYPTION_KEY=<app-encryption-key secret> OCPP_PUBLIC_URL=wss://<your-domain>/ocppWatch out Never commit server.env to a Git repository or paste it into a message: it holds your secrets.
Start it and sign in
The moment you've been waiting for. The first start builds the images on the instance, so it takes a little longer than the ones after it.
5.1Start the services
From the deploy folder, build and start everything, then follow the server logs. On first start the server creates the tables in the database and the admin account.
Terminal docker compose up -d --build docker compose logs -f serverWhat you should see The logs scroll, then settle down with no errors. Leave with Ctrl+C: the services keep running.
5.2Sign in to the dashboard
Open https://<your-domain>/auth/login in your browser. The sign-in screen asks for three things: the workspace ID (your BOOTSTRAP_TENANT), the admin email and the passphrase. The dashboard itself is in French.
Once you're in, open Espace client (workspace settings) to name your workspace and upload your logo. It will show in the dashboard and on the payment portal.
What you should see The browser shows a padlock (valid certificate) and you land in your workspace's dashboard.
5.3Remove the first-run credentials
The account now lives in the database. Delete BOOTSTRAP_PASSWORD (and BOOTSTRAP_EMAIL) from server.env, then restart so the server runs without them.
Terminal docker compose up -d --buildWhat you should see You can still sign in normally with your admin account.
Connect a charger
A charger opens a permanent connection to your server and keeps it open. All it needs is an address and a password.
6.1Add the charger
In the dashboard, add the charger with its ID. Its password is shown only once, so copy it right away. If you lose it, the charger's page lets you generate a new one.
6.2Configure the charger
In the charger's configuration screen, choose OCPP 1.6 JSON, the ocpp1.6 subprotocol and Basic authentication. <workspace> is your workspace ID, <charger-id> the ID you just added. The charger usually builds the Authorization header itself from the ID and password you enter.
Charger settings wss://<your-domain>/ocpp/<workspace>/<charger-id> Authorization: Basic <base64(charger-id:password)>What you should see The charger shows as online in the dashboard. If it stays offline, open its history: a CONNECTION_REJECTED event means the connection was refused, often because of a wrong password or address.
Payments with Stripe
Optional. Stripe enables QR code payment without an account, driver billing and charging card sales. Each workspace connects its own Stripe account, and the money lands straight in it.
7.1Add the keys
Start with Stripe's test keys, then switch to live keys once everything checks out. In Stripe, create an event destination for connected accounts, pointing at the address below, with the seven events listed. Stripe then gives you the signing secret to put in STRIPE_WEBHOOK_SECRET.
Webhook address https://<your-domain>/api/backend/api/v1/stripe/webhookEvents checkout.session.completed checkout.session.async_payment_succeeded checkout.session.async_payment_failed checkout.session.expired invoice.paid invoice.payment_failed invoice.finalizeddeploy/server.env STRIPE_SECRET_KEY=<Stripe secret key> STRIPE_WEBHOOK_SECRET=<signing secret> STRIPE_PUBLISHABLE_KEY=<Stripe publishable key>7.2Restart and turn on public payment
Restart with docker compose up -d --build. In the dashboard, create a rate in euros under Tarifs (rates), connect the workspace's Stripe account under Paiements (payments) and finish verification with Stripe, then turn on public payments.
Under Points de charge publics (public charge points), link a charger, a connector and a rate, publish the portal and download its QR code. Drivers scan it, pick a credit between €5 and €100 and pay, without creating an account.
What you should see In Stripe, the destination's deliveries tab shows a test event delivered successfully.
Optional but handy: password resets, alerts and notifications all go out by email.
8.1Send through OCI Email Delivery
On Oracle, outbound port 25 is blocked by default. Use OCI Email Delivery over SMTP on port 587 with STARTTLS. Set up SPF, DKIM and DMARC for the sender address, or your emails may end up in spam.
Fill in the email block of server.env with the SMTP server and credentials Email Delivery gives you, then restart with docker compose up -d --build.
deploy/server.env MAIL_ENABLED=true MAIL_FROM=<sender-address> SMTP_HOST=<Email Delivery SMTP server> SMTP_PORT=587 SMTP_USER=<SMTP user> SMTP_PASSWORD=<SMTP password> SMTP_AUTH=true SMTP_STARTTLS=trueWhat you should see Request a password reset from the sign-in screen: the email arrives.
Watch out Outbound port 25 is blocked by default on Oracle. Don't try to send directly, go through port 587.
Check, back up, update
Your platform is running. A few checks before you connect the whole fleet, then the two habits that keep it healthy.
9.1Check from the internet
Run these three commands from your computer. Replace <your-domain> and <workspace>. UNKNOWN-CHARGER can stay as it is.
Terminal curl -sSI https://<your-domain>/auth/login curl -sS -o /dev/null -w '%{http_code}\n' https://<your-domain>/actuator/health curl --http1.1 -sS -o /dev/null -w '%{http_code}\n' \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \ -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' -H 'Sec-WebSocket-Protocol: ocpp1.6' \ https://<your-domain>/ocpp/<workspace>/UNKNOWN-CHARGERWhat you should see The sign-in page answers 200, /actuator/health answers 404 (it must not be visible from the internet) and the charger connection without credentials answers 401. Then a test charger should stay connected for more than an hour with no reconnection in its history.
9.2Back up
With the managed database, daily backups are already on. Add an exported copy outside your Oracle account, and try a restore at least once a quarter on a separate environment. Keep APP_ENCRYPTION_KEY in the vault and as an offline copy.
With the local database, the command below produces a full copy of the database. Keep it off the instance.
Terminal docker compose exec postgres pg_dump -U ev_supervision ev_supervision9.3Update
A new version goes in two moves: pull the code, rebuild. Database migrations run on start, so back up the database before each update and pick a quiet moment.
During the restart, chargers disconnect and then reconnect on their own. The messages they held back are replayed, and duplicates are recognised.
Terminal git pull && docker compose up -d --buildWhat you should see Chargers come back online in the dashboard after the restart.
The variables you set
Only the ones in this guide. The example files hold others (mobile apps, roaming, advanced security profiles) that you can leave as they are.
| Variable | What it does | Status |
|---|---|---|
APP_HOST | Your domain name (.env). Used by the dashboard, the API, Stripe notifications and the chargers.supervision.exemple.fr | Required |
ACME_EMAIL | Contact address for Let's Encrypt certificates (.env).exploitation@exemple.fr | Required |
COMPOSE_PROFILES | Empty with the managed database. local-db runs PostgreSQL on the instance, for a trial (.env). | Required |
DATABASE_URL | Address of the managed database (.env). Not needed with the local database.jdbc:postgresql://10.0.0.5:5432/ev_supervision?sslmode=require | Required |
DATABASE_USER | User that owns the database (.env).ev_supervision | Required |
DATABASE_PASSWORD | Database password, stored in OCI Vault (.env). | Required |
BOOTSTRAP_TENANT | ID of the first workspace, typed at sign-in. Choose it before the first start (server.env).codetics | Required |
BOOTSTRAP_EMAIL | Email of the first admin (server.env). Remove it after your first sign-in.administrateur@exemple.fr | Required |
BOOTSTRAP_PASSWORD | Passphrase of the first admin (server.env). Remove it after your first sign-in. | Required |
APP_ENCRYPTION_KEY | 32 random bytes in Base64 that encrypt secrets in the database (server.env). Needed for Stripe. Losing it makes that data unreadable. | Required |
OCPP_PUBLIC_URL | Charger address shown on each charger's commissioning sheet (server.env).wss://supervision.exemple.fr/ocpp | Optional |
STRIPE_SECRET_KEY | Stripe secret key, test first, then live (server.env). | Optional |
STRIPE_WEBHOOK_SECRET | Signing secret of the Stripe event destination (server.env). | Optional |
STRIPE_PUBLISHABLE_KEY | Stripe publishable key, needed for drivers to save payment methods (server.env). | Optional |
MAIL_ENABLED | Turns email sending on (server.env).true | Optional |
MAIL_FROM | Sender address, on your domain (server.env).supervision@exemple.fr | Optional |
SMTP_HOST | SMTP server given by OCI Email Delivery (server.env). | Optional |
SMTP_PORT | SMTP port. Port 25 is blocked on Oracle (server.env).587 | Optional |
SMTP_USER | SMTP user given by Email Delivery (server.env). | Optional |
SMTP_PASSWORD | SMTP password given by Email Delivery (server.env). | Optional |