Skip to content

Install Luvio with Docker

This guide runs the Luvio server in containers on one Linux machine with Docker. Two layouts are available:

Layout What runs When to choose it
Docker Compose (recommended) Three containers: Luvio, its template transfer worker and PostgreSQL 17 Most installations. The database is a standard PostgreSQL container you can back up with the usual tools.
Single container One container with Luvio and its own PostgreSQL A test, a demonstration, or when one container is all you can run.

At the end of this guide, the Luvio web console answers on http://<docker host>:3080 and you go on with First setup.

To run Luvio on several Docker hosts for high availability, see Docker application nodes.

You need:

  • A Linux machine with Docker Engine 24 or later and the Compose plugin 2.24 or later. Check them with docker version and docker compose version. This guide was validated on Ubuntu 24.04 LTS with Docker 28 and Compose 2.
  • At least 2 vCPU, 4 GB of RAM and 40 GB of disk for Docker’s volumes. Plan more disk if you use Universal Templates (see Plan your installation).
  • A fixed IP address or a DNS name for the machine: the virtual machines connect back to it. You give it to Luvio at step 2.
  • The time of the machine synchronized with NTP.
  • TCP port 3080 free on the machine.

Without Internet access (offline bundle). Download luvio-docker-<version>-offline.tar.gz from <DOWNLOAD_URL> and copy it to the Docker machine. It holds the images of Luvio and of PostgreSQL 17 for x86-64 processors, and the docker-compose.yml file. Unpack it in the directory that will hold the installation, then load the images:

Terminal window
sudo mkdir -p /opt/luvio && cd /opt/luvio
sudo tar --no-same-owner -xzf ~/luvio-docker-<version>-offline.tar.gz
sudo docker load -i luvio-docker-<version>.tar

docker load prints Loaded image: luvio:<version> and Loaded image: postgres:17.

With access to the registry. Download docker-compose.yml from <DOWNLOAD_URL> into /opt/luvio, then replace luvio:<version> with <REGISTRY>/luvio:<version> in its two image: lines. docker compose downloads the images at the next step.

In /opt/luvio, create the .env file. Docker Compose reads it by itself. Replace luvio.example.com with the DNS name or the IP address of the Docker machine, as the virtual machines reach it, without http:// and without a port:

Terminal window
cd /opt/luvio
sudo sh -c 'cat > .env <<EOF
LUVIO_PUBLIC_HOSTNAME=luvio.example.com
LUVIO_DB_PASSWORD=$(openssl rand -hex 24)
LUVIO_SESSION_SECRET=$(openssl rand -hex 48)
LUVIO_CONFIG_ENCRYPTION_KEY=$(openssl rand -hex 48)
EOF'
sudo chmod 600 .env

LUVIO_PUBLIC_HOSTNAME is the Server URL that Luvio gives to the agents in the virtual machines. Inside its container, Luvio only sees the container’s own address (for example 172.18.0.3), which the virtual machines cannot reach.

LUVIO_CONFIG_ENCRYPTION_KEY encrypts the hypervisor passwords and the other secrets Luvio stores in its database. Without it, a backup of the database cannot be restored, and it must never change. Copy .env to your password safe or your backup system now.

Terminal window
cd /opt/luvio
sudo docker compose up -d

The three containers restart by themselves with the machine.

  1. List the containers:

    Terminal window
    sudo docker compose ps

    The three containers must be Up. After a minute, db and luvio also show (healthy).

  2. Check that the web server answers:

    Terminal window
    curl -fsS http://localhost:3080/readyz

    The answer starts with {"status":"ready": Luvio has built its database at its first start. If it is {"status":"not_ready"..., wait a minute, then read the logs (Troubleshooting).

  3. From your workstation, open http://<docker host>:3080. The Welcome to Luvio page appears.

Docker opens port 3080 itself in the machine’s packet filter: you do not open it in firewalld or ufw. A network firewall between the users and the machine must still allow it.

Open http://<docker host>:3080 and follow First setup. Luvio has built its database already: the wizard opens on the hypervisor. In Administration > Server & storage, the Server URL shows http://<LUVIO_PUBLIC_HOSTNAME>:3080 with From environment.

To skip the wizard, give the hypervisor and the administrator in a configuration file.

The settings are in the environment: sections of docker-compose.yml, and the address in .env. After a change, apply it:

Terminal window
sudo docker compose up -d
Setting Default Use
LUVIO_PUBLIC_HOSTNAME (in .env) empty DNS name or IP address of the Docker machine, the Server URL of the agents. Empty: set the Server URL in Administration > Server & storage instead
SERVER_PROTOCOL http http, https or both
HTTP_PORT 3080 Port of the HTTP listener
HTTPS_PORT 3443 Port of the HTTPS listener
TRUSTED_PROXIES empty Addresses of the reverse proxies in front of Luvio: see Behind a reverse proxy
LUVIO_CONFIG_FILE (in .env) empty The configuration file: /etc/luvio/config/luvio-config.yaml
LUVIO_ADMIN_PASSWORD (in .env) empty Password of the first admin account, created only when there is none
LUVIO_NO_HYPERVISOR (in .env) empty true (or 1, yes; false, 0, no or empty: off, any other value is an error): physical machines only, no hypervisor (hypervisors: []). With LUVIO_ADMIN_PASSWORD, the installation is complete without the setup wizard
LUVIO_TLS_CERT_FILE, LUVIO_TLS_KEY_FILE (in .env) empty Your certificate for HTTPS: see Switch to HTTPS. You can also import one in Administration > Server & storage > HTTPS certificate; these files win over it

The port Luvio listens on must be published under the same number ("3080:3080"): Luvio tells the agents to connect to it. That is why the protocol and the ports are set here, with the published ports, and not in the web console: Administration > Server & storage shows them with From environment.

In the luvio service, set SERVER_PROTOCOL: both, add HTTPS_PORT: "3443", and publish the port:

ports:
- "3080:3080"
- "3443:3443"

Then run sudo docker compose up -d. Keep HTTP on as well (both): the health check of the container uses it. The agents use HTTPS once it is on.

With both, port 3080 stays open for the agents, the desktop clients, the identity providers and the health check, and the web console moves to HTTPS: a browser that opens http://<docker host>:3080 is sent to https://<docker host>:3443, and the sign-in and the console’s API refuse HTTP. A session opened over HTTPS has its own cookie, which browsers never send over HTTP. HTTP requests from inside the container, and those of a trusted reverse proxy (below), still reach the console over HTTP.

HTTPS answers tell the browser to use HTTPS for this host name for one day (HSTS). Browsers ignore this for an IP address and for a certificate they do not trust. When they apply it, they open every http:// address of that host name over HTTPS, port 3080 included, which fails: type https:// addresses in the browser. Single sign-on to the web console comes back to the HTTPS address: the identity provider must accept https://<docker host>:3443/api/auth/oidc/callback (OpenID Connect) or the SAML metadata of https://<docker host>:3443/api/auth/saml/metadata, and a Redirect URI set in the OpenID Connect settings must be that HTTPS address.

By default, Luvio serves HTTPS with a self-signed certificate that it creates and renews itself: browsers show a warning, and the desktop clients ask the user to trust it the first time. To serve your own certificate, valid for LUVIO_PUBLIC_HOSTNAME:

  1. Put the certificate (PEM, with its chain) and its key in /opt/luvio/tls, readable by the user of the container:

    Terminal window
    cd /opt/luvio
    sudo mkdir -p tls
    sudo cp luvio.crt tls/tls.crt
    sudo cp luvio.key tls/tls.key
    sudo chown -R 1000:1000 tls
    sudo chmod 600 tls/tls.key
  2. In docker-compose.yml, remove the # in front of the ./tls line of the luvio service: - ./tls:/etc/luvio/tls:ro,z.

  3. Add to .env:

    Terminal window
    LUVIO_TLS_CERT_FILE=/etc/luvio/tls/tls.crt
    LUVIO_TLS_KEY_FILE=/etc/luvio/tls/tls.key
  4. Run sudo docker compose up -d.

Luvio reads the files again when they change (a renewal), without a restart.

A reverse proxy can serve the web console with your certificate. In the luvio service, set TRUSTED_PROXIES to the address the proxy connects from, as Luvio sees it: IP addresses or networks (CIDR), separated by commas. For a proxy in another container of the same Compose project, it is the subnet of the project’s network (sudo docker network inspect <project>_default); for a proxy on the Docker machine itself, the gateway of that network.

TRUSTED_PROXIES: "172.18.0.0/16"

Luvio then reads the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers that the proxy adds: it sees the users’ own addresses (the sign-in limits count per address), and knows when the proxy serves HTTPS. A session opened through an HTTPS proxy then gets a Secure cookie, and the answers carry HSTS. Luvio ignores these headers from any other address. With both, a browser that reaches port 3080 directly is still sent to HTTPS; requests of a trusted proxy are served, over HTTP or HTTPS as the proxy reports: make the proxy send its own HTTP visitors to HTTPS.

Without the setting, every user has the address of the proxy, and a session opened through an HTTPS proxy gets a cookie without Secure. Trust nothing but the proxy: a client that connects from a trusted address can choose the address and the protocol that Luvio sees.

To use a PostgreSQL server of your own (12 or later): remove the db service and its depends_on entries from docker-compose.yml, then set PGHOST, PGPORT, PGDATABASE, PGUSER (in x-luvio-environment at the top of the file) and LUVIO_DB_PASSWORD (in .env) to that server. The account needs the CREATEDB right, unless the database exists already and belongs to it: Luvio builds the database at its first start. Connect it to PostgreSQL directly, never through PgBouncer in transaction mode.

  1. Take a backup.

  2. Load the new images (docker load -i luvio-docker-<new version>.tar), or skip this step with a registry.

  3. In docker-compose.yml, change the tag of the two image: lines to the new version.

  4. Apply:

    Terminal window
    sudo docker compose up -d

Keep .env and the volumes: they hold the keys and the data.

Then upgrade the agents: the Luvio server and its agents must be the same release, and an agent older than the server refuses its settings and its commands (see Upgrade Luvio and every agent together in Templates, agent and pools). With application nodes, upgrade every node before the agents.

Universal Templates on Proxmox VE: from a version that did not check the SSH host keys of the nodes, copies stop until you trust the key of each node, in the Universal Templates panel of the hypervisor (see Universal Templates).

A backup is three things: .env, the database, and the luvio-data volume (the identity of the server, its HTTPS certificate, the uploaded files). Docker Compose names the volumes after the directory, here luvio_luvio-data and luvio_luvio-db.

Terminal window
cd /opt/luvio
sudo docker compose exec -T db pg_dump -U luvio -Fc luvio > ~/luvio-db-$(date +%F).dump
sudo docker run --rm -v luvio_luvio-data:/var/lib/luvio --entrypoint tar luvio:<version> \
-czf - -C /var/lib/luvio . > ~/luvio-data-$(date +%F).tar.gz
sudo cat .env > ~/luvio-env-$(date +%F)

Keep the three files outside the machine: .env holds the keys.

To restore them on a new machine: install as above with the saved .env (not a new one), but instead of docker compose up -d, create the containers, start only the database, restore it, then start everything:

Terminal window
cd /opt/luvio
sudo docker compose create
sudo docker compose up -d db
sudo docker compose exec -T db pg_restore -U luvio -d luvio --clean --if-exists < luvio-db-<date>.dump
sudo docker run --rm -i -v luvio_luvio-data:/var/lib/luvio --entrypoint tar luvio:<version> \
-xzf - -C /var/lib/luvio < luvio-data-<date>.tar.gz
sudo docker compose up -d

A restore brings Luvio back to the time of the backup: desktops created after it are unknown to Luvio; delete them from the hypervisor.

Terminal window
cd /opt/luvio
sudo docker compose down

This removes the containers and keeps the volumes: docker compose up -d brings Luvio back with its data. To delete the data too, after a backup, run sudo docker compose down -v.

The single-container image runs Luvio, its template transfer worker and its own PostgreSQL 17 in one container. It generates its secrets at the first start and keeps them in its volume.

  1. Load the image from luvio-docker-<version>-standalone-offline.tar.gz:

    Terminal window
    tar -xzf luvio-docker-<version>-standalone-offline.tar.gz
    sudo docker load -i luvio-docker-<version>-standalone.tar

    With a registry, the image is <REGISTRY>/luvio:<version>-standalone.

  2. Start it:

    Terminal window
    sudo docker run -d --name luvio --restart unless-stopped \
    -p 3080:3080 \
    -e PUBLIC_HOSTNAME=luvio.example.com \
    -v luvio-data:/var/lib/luvio \
    luvio:<version>-standalone

    Replace luvio.example.com with the DNS name or the IP address of the Docker machine, as the virtual machines reach it (see step 2).

  3. Check it with curl -fsS http://localhost:3080/readyz ({"status":"ready"...), then follow First setup.

The secrets are in /var/lib/luvio/luvio.env inside the volume. Save them:

Terminal window
sudo docker exec luvio cat /var/lib/luvio/luvio.env

To upgrade, load the new image, then remove the container and start it again with the new tag, the same volume and the same PUBLIC_HOSTNAME. To back up, stop the container and save the volume:

Terminal window
sudo docker stop luvio
sudo docker run --rm -v luvio-data:/var/lib/luvio --entrypoint tar luvio:<version>-standalone \
-czf - -C /var/lib/luvio . > ~/luvio-standalone-$(date +%F).tar.gz
sudo docker start luvio

Read the logs first:

Terminal window
sudo docker compose logs -f luvio # web console, API
sudo docker compose logs -f luvio-transfer # template transfers
sudo docker compose logs -f db # PostgreSQL
Symptom Cause and fix
required variable LUVIO_DB_PASSWORD is missing a value The .env file is missing or is not in the directory of docker-compose.yml. Create it (step 2).
The desktops stay Not connected; the Server URL is http://172.x.x.x:3080 with the warning The virtual machines probably cannot reach this address LUVIO_PUBLIC_HOSTNAME is missing from .env, or is not a name or an address (the logs of luvio then say PUBLIC_HOSTNAME="…" is not a host name or an IP address). Fix it and run sudo docker compose up -d, or set the Server URL in Administration > Server & storage. Luvio sends the new address to the agents that were given the container’s address. A desktop that stays Not connected: select it in Manage Machines and use Resend Agent Config.
Bind for 0.0.0.0:3080 failed: port is already allocated Another program or container uses port 3080. Stop it, or change HTTP_PORT and the published port together.
luvio is unhealthy after a switch to HTTPS only The health check uses HTTP: set SERVER_PROTOCOL: both.
password authentication failed for user "luvio" in the logs of luvio LUVIO_DB_PASSWORD changed after the database was created: PostgreSQL keeps the first password. Put back the original .env.
Unsupported state or unable to authenticate data LUVIO_CONFIG_ENCRYPTION_KEY is not the key the database was encrypted with. Put back the original .env.