vcon-server

Run it in a container

Build a container image from a checked release, keep the configuration and the data in volumes, allow the server to lock its memory, make the first keys, run it, back it up, and stop it, with Docker or Docker Compose.

The server runs in a container as it runs on a host, with one difference: a container's memory-lock limit is a ceiling that nothing inside it can raise. The server locks all of its memory at start and refuses to start if it cannot, so every container that runs vcon-server needs --ulimit memlock=-1.

This how-to builds an image from a release, checked against its checksum while it is built. It keeps the configuration, the data and the backups in three named volumes, and runs the server with Docker, then with Docker Compose.

CI runs every command on this page with Docker on each change, and checks each answer.

Before you start

  • Docker, and a user that may run it (a member of the docker group, or root). docker version answers.
  • openssl, curl and jq on the host, for the certificate and the checks. On Debian or Ubuntu: sudo apt-get install -y openssl curl jq.
  • One shell for every step: later steps use variables that earlier steps set.

Work in a new directory:

mkdir vcon-container && cd vcon-container

Build the image

Save this as Dockerfile. The first stage downloads the release for the build host's CPU and checks it against SHA256SUMS.txt. The build stops if the checksum does not match. The second stage holds only Debian's base, the binary and the vcon account:

FROM debian:trixie-slim AS download
ARG VERSION
RUN apt-get update -qq \
 && apt-get install -y -qq --no-install-recommends curl ca-certificates >/dev/null
WORKDIR /download
RUN archive="vcon-server-${VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" \
 && curl -fsSLO "https://github.com/NormB/vcon-server/releases/download/v${VERSION}/${archive}" \
 && curl -fsSLO "https://github.com/NormB/vcon-server/releases/download/v${VERSION}/SHA256SUMS.txt" \
 && sha256sum -c SHA256SUMS.txt --ignore-missing \
 && tar -xzf "${archive}" \
 && mv "${archive%.tar.gz}/vcon-server" /download/vcon-server

FROM debian:trixie-slim
COPY --from=download /download/vcon-server /usr/local/bin/vcon-server
RUN useradd --system --user-group --uid 10001 --no-create-home --home-dir /var/lib/vcon-server \
      --shell /usr/sbin/nologin vcon \
 && install -d -m 0700 -o vcon -g vcon /var/lib/vcon-server /etc/vcon-server /backup
USER vcon
EXPOSE 8443
ENTRYPOINT ["vcon-server"]
CMD ["serve", "--config", "/etc/vcon-server/vcon-server.toml"]

Build it, naming the version:

VERSION=0.4.0
docker build -q --build-arg VERSION=$VERSION -t vcon-server:$VERSION . >/dev/null
docker run --rm vcon-server:$VERSION --version
vcon-server <version>

docker build without -q shows each step, and sha256sum prints the archive's name and OK.

Make the volumes

Three named volumes hold what must outlive a container: the configuration with the master key, the data directory, and the backups. A named volume takes the owner and the mode of the image's directory the first time a container mounts it, so each starts out owned by vcon with mode 0700:

docker volume create vcon-etc >/dev/null
docker volume create vcon-data >/dev/null
docker volume create vcon-backup >/dev/null
docker volume inspect vcon-etc vcon-data vcon-backup | jq -r '.[].Name'
vcon-etc
vcon-data
vcon-backup

Keep vcon-etc and vcon-backup apart when you copy them off the host: one holds the master key, the other holds data that the master key opens.

Fill the configuration volume

vcs runs a vcon-server command in a new container with both volumes and the memory-lock limit, as vcon. The steps below use it:

vcs() {
  docker run --rm -i --ulimit memlock=-1 \
    -v vcon-etc:/etc/vcon-server -v vcon-data:/var/lib/vcon-server -v vcon-backup:/backup \
    "$@"
}

Make the master key inside the volume, so it never sits on the host:

vcs --entrypoint sh vcon-server:$VERSION -c \
  'head -c 32 /dev/urandom > /etc/vcon-server/master.key && chmod 0400 /etc/vcon-server/master.key'

Make a certificate for the name clients use, here a self-signed one for vcon.example.com (see getting started for its extensions), and copy it in with the private key:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
  -subj /CN=vcon.example.com -addext subjectAltName=DNS:vcon.example.com \
  -addext basicConstraints=critical,CA:FALSE -addext keyUsage=critical,digitalSignature -addext extendedKeyUsage=serverAuth \
  -keyout privkey.pem -out fullchain.pem 2>/dev/null
vcs --entrypoint sh vcon-server:$VERSION -c 'cat > /etc/vcon-server/cert.pem' < fullchain.pem
sudo install -d -m 0755 /usr/local/share/vcon-server
sudo install -m 0644 fullchain.pem /usr/local/share/vcon-server/container-ca.pem
vcs --entrypoint sh vcon-server:$VERSION -c 'umask 077; cat > /etc/vcon-server/key.pem' < privkey.pem
rm privkey.pem

The copy at /usr/local/share/vcon-server/container-ca.pem is for clients on this host: the curl commands below pass it by its absolute path, so they work from any directory. "Remove everything" deletes it.

Write the configuration, the same as on a host, and copy it in:

listen = "0.0.0.0:8443"
data_dir = "/var/lib/vcon-server"
master_key = "/etc/vcon-server/master.key"

[tls]
cert = "/etc/vcon-server/cert.pem"
key = "/etc/vcon-server/key.pem"
vcs --entrypoint sh vcon-server:$VERSION -c 'cat > /etc/vcon-server/vcon-server.toml' < vcon-server.toml
vcs --entrypoint ls vcon-server:$VERSION -l /etc/vcon-server | awk 'NR > 1 { print $1, $3, $NF }'
-rw-r--r-- vcon cert.pem
-rw------- vcon key.pem
-r-------- vcon master.key
-rw-r--r-- vcon vcon-server.toml

Make the first tenant and its keys

tenant create runs only while no server does, so make the first tenant before you start one:

TENANT=$(vcs vcon-server:$VERSION tenant create acme --config /etc/vcon-server/vcon-server.toml | jq -r .id)
OPS=$(vcs vcon-server:$VERSION key create --operator --label ops --config /etc/vcon-server/vcon-server.toml | jq -r .token)
MONITOR=$(vcs vcon-server:$VERSION key create --tenant "$TENANT" --scope read --label monitor \
  --config /etc/vcon-server/vcon-server.toml | jq -r .token)
vcs vcon-server:$VERSION key list --config /etc/vcon-server/vcon-server.toml | jq -c '{label, scopes}'
{"label":"ops","scopes":["admin"]}
{"label":"monitor","scopes":["read"]}

Without the memory-lock limit

A container started without --ulimit memlock=-1 cannot lock the memory, and every command, serve included, refuses:

docker run --rm -v vcon-etc:/etc/vcon-server -v vcon-data:/var/lib/vcon-server \
  vcon-server:$VERSION check --config /etc/vcon-server/vcon-server.toml
vcon-server: memory cannot be locked: Cannot allocate memory (os error 12); allow it, for example LimitMEMLOCK=infinity

Run the server

Start it in the background, publishing port 8443. --restart unless-stopped starts it again after a crash or a reboot of the host:

docker run -d --name vcon-server --restart unless-stopped --ulimit memlock=-1 -p 8443:8443 \
  -v vcon-etc:/etc/vcon-server -v vcon-data:/var/lib/vcon-server -v vcon-backup:/backup \
  vcon-server:$VERSION >/dev/null
timeout 30 sh -c 'until docker logs vcon-server 2>&1 | grep -q serving; do sleep 1; done'
docker logs vcon-server 2>&1 | grep vcon_server
<time>  INFO vcon_server::serve: serving listen=0.0.0.0:8443
<time>  INFO vcon_server::serve: purge sweep purged=0 failed=0

The server logs to standard error, which docker logs shows.

Check that it answers

The certificate names vcon.example.com. --resolve points that name at this host for one request, without a DNS entry:

curl -sS --cacert /usr/local/share/vcon-server/container-ca.pem --resolve vcon.example.com:8443:127.0.0.1 \
  https://vcon.example.com:8443/health -H "Authorization: Bearer $MONITOR"
{"status":"ok"}

Run commands beside it

The commands that run beside the server run in its container with docker exec. They take the container's memory-lock limit and its user:

WRITER=$(docker exec vcon-server vcon-server key create --tenant "$TENANT" --scope write --label recorder-1 \
  --config /etc/vcon-server/vcon-server.toml | jq -r .token)
docker exec vcon-server vcon-server key list --tenant "$TENANT" --config /etc/vcon-server/vcon-server.toml | jq -r .label
monitor
recorder-1

Store your first vCon works the same against this server, with --resolve vcon.example.com:8443:127.0.0.1 added to each curl.

Back it up

backup writes into the backup volume while the server runs:

BACKUP=/backup/data-$(date -u +%Y%m%dT%H%M%SZ)
docker exec vcon-server vcon-server backup "$BACKUP" --config /etc/vcon-server/vcon-server.toml
{"tenants":1,"tries":1,"versions":0}

Copy the backup volume off the host, and the master key apart from it. docker run --rm -v vcon-backup:/backup debian:trixie-slim tar -C /backup -c . writes the volume to standard output as a tar archive. See back up and restore for the restore.

Stop it and start it

docker stop sends SIGTERM. The server logs stopped and exits:

docker stop vcon-server >/dev/null
docker logs vcon-server 2>&1 | grep -c 'vcon_server::serve: stopped'
1

The data stays in its volume. Start the same container again:

docker start vcon-server >/dev/null
timeout 30 sh -c 'until curl -s -o /dev/null --cacert /usr/local/share/vcon-server/container-ca.pem --resolve vcon.example.com:8443:127.0.0.1 https://vcon.example.com:8443/health; do sleep 1; done'
curl -sS --cacert /usr/local/share/vcon-server/container-ca.pem --resolve vcon.example.com:8443:127.0.0.1 \
  https://vcon.example.com:8443/health -H "Authorization: Bearer $MONITOR"
{"status":"ok"}

Or with Docker Compose

The same container as a Compose service. The volumes are the ones made above, so Compose uses them as they are (external: true). Save this as compose.yaml:

services:
  vcon-server:
    image: vcon-server:${VERSION}
    restart: unless-stopped
    ulimits:
      memlock: -1
    ports:
      - "8443:8443"
    volumes:
      - vcon-etc:/etc/vcon-server
      - vcon-data:/var/lib/vcon-server
      - vcon-backup:/backup
volumes:
  vcon-etc:
    external: true
  vcon-data:
    external: true
  vcon-backup:
    external: true

Remove the container started by hand, then start the service. Compose reads VERSION from the environment, which env sets for it:

docker rm -f vcon-server >/dev/null
env VERSION=$VERSION docker compose up -d --quiet-pull 2>/dev/null
timeout 30 sh -c 'until curl -s -o /dev/null --cacert /usr/local/share/vcon-server/container-ca.pem --resolve vcon.example.com:8443:127.0.0.1 https://vcon.example.com:8443/health; do sleep 1; done'
curl -sS --cacert /usr/local/share/vcon-server/container-ca.pem --resolve vcon.example.com:8443:127.0.0.1 \
  https://vcon.example.com:8443/health -H "Authorization: Bearer $MONITOR"
{"status":"ok"}

Stop and remove the service. The volumes stay:

env VERSION=$VERSION docker compose down 2>/dev/null
docker volume inspect vcon-data | jq -r '.[].Name'
vcon-data

With the objects in an S3 bucket

The image above, built from release 0.4.0 or later, can keep its objects in an S3 bucket (see keep vCons in an S3 bucket); release 0.3.0 cannot. For a bucket:

  • put the secret key file and the storage service's certificate authority in the vcon-etc volume, owned by vcon with mode 0400, and name them in storage.s3.secret_access_key_file and storage.s3.ca_file by their paths in the container, such as /etc/vcon-server/s3.secret;
  • make sure the container can reach the service's address and port: with objects.example.com resolved by the host's DNS, or with --add-host objects.example.com:<address> on each docker run;
  • run every command the same way: each one, backup and check included, reaches the bucket at start (see the command line).

Keep vCons in an S3 bucket sets up the bucket, its key and the configuration. This container path was not run by CI.

Remove everything

This deletes the data and the master key with their volumes. Do it only for a server you are trying out:

docker rm -f vcon-server >/dev/null 2>&1
docker volume rm vcon-etc vcon-data vcon-backup >/dev/null
docker image rm vcon-server:$VERSION >/dev/null
sudo rm -f /usr/local/share/vcon-server/container-ca.pem
cd .. && rm -r vcon-container

Source of this page: website/content/docs/container.md