Skip to content

Podman

Deploy Team Server with Podman Compose and a local PostgreSQL container, or run it rootless against an existing PostgreSQL service.

Prerequisites

  • Podman 4.4 or later
  • A Compose provider for the Compose method
  • PostgreSQL 18 or a compatible managed PostgreSQL service
  • At least 2 GB of memory and 10 GB of disk for Team Server
  • A Team Server license and an OIDC application for production authentication
  • A public HTTPS URL reachable by users and Sensors

For rootless services that must survive logout and reboot, enable lingering:

sudo loginctl enable-linger "$USER"

Prepare Configuration

Both deployment methods use the same project directory, configuration, and environment file.

mkdir -p team-server/config
cd team-server

Save the production configuration example as config/production.yaml. Before deployment:

  1. Add the discovery jobs for the CI/CD providers you use.
  2. Configure at least one OIDC provider.
  3. Add outbound HTTP policy for private integration endpoints or internal certificate authorities.
  4. Confirm that server.host, CORS, and settings.tenant.base_url use SERVER_URL.

Generate two distinct JWT secrets and a database password:

openssl rand -base64 48
openssl rand -base64 48
openssl rand -hex 24

Create .env:

POSTGRES_DB=team_server
POSTGRES_USER=team_server
POSTGRES_PASSWORD=<generated_database_password>
DATABASE_URL=postgresql://team_server:<generated_database_password>@postgres:5432/team_server

ENDURA_LICENSE_KEY=<license_key>
JWT_SENSOR_SECRET=<first_base64_secret>
JWT_USER_SECRET=<second_base64_secret>
SERVER_URL=https://team-server.example.com
TENANT_NAME=Example Organization

GOOGLE_OIDC_CLIENT_ID=<client_id>
GOOGLE_OIDC_CLIENT_SECRET=<client_secret>

Use the variables referenced by your selected OIDC provider, then protect the file:

chmod 600 .env
printf '%s\n' '.env' >> .gitignore

Each JWT secret must decode to at least 32 bytes, and the values must differ.

Method 1: Podman Compose

Create podman-compose.yaml:

services:
  postgres:
    image: docker.io/postgres:18-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  endura-team-server:
    image: ghcr.io/endurasecurity/container/endura-team-server:testing
    env_file: .env
    ports:
      - "5150:5150"
    volumes:
      - ./config:/app/config:ro,Z
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5150/_readiness"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    restart: unless-stopped

volumes:
  postgres_data:

Start and verify the services:

podman compose -f podman-compose.yaml up -d
podman compose -f podman-compose.yaml logs -f endura-team-server
podman compose -f podman-compose.yaml ps
curl -f http://localhost:5150/_readiness

Readiness returns HTTP 200 with {"ok":true} after startup completes.

Method 2: Existing PostgreSQL

Update DATABASE_URL in .env with the external database host, then run Team Server rootless:

podman run -d \
  --name endura-team-server \
  --userns=keep-id \
  --env-file .env \
  -p 5150:5150 \
  -v "$PWD/config:/app/config:ro,Z" \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

Verify startup:

podman ps --filter name=endura-team-server
podman logs endura-team-server
curl -f http://localhost:5150/_readiness

Run Direct Podman with Quadlet

Quadlet gives a rootless direct deployment a persistent user service. Use absolute paths for the environment file and configuration directory.

Create ~/.config/containers/systemd/endura-team-server.container:

[Unit]
Description=Endura Team Server
Wants=network-online.target
After=network-online.target

[Container]
Image=ghcr.io/endurasecurity/container/endura-team-server:testing
ContainerName=endura-team-server
EnvironmentFile=/absolute/path/to/team-server/.env
Volume=/absolute/path/to/team-server/config:/app/config:ro,Z
PublishPort=5150:5150
UserNS=keep-id

[Service]
Restart=always

[Install]
WantedBy=default.target

Remove a manually started container with the same name, then start the service:

podman rm --force endura-team-server
systemctl --user daemon-reload
systemctl --user enable --now endura-team-server.service
systemctl --user status endura-team-server.service

View service logs with journalctl --user -u endura-team-server.service -f.

Configure HTTPS

Production deployments must expose SERVER_URL over HTTPS. Terminate TLS at a trusted reverse proxy or load balancer, or configure Team Server directly:

settings:
  tls:
    certificate: "/run/secrets/server_cert"
    private_key: "/run/secrets/server_key"

Mount the certificate and key into the Team Server container:

-v /absolute/path/server.pem:/run/secrets/server_cert:ro,Z
-v /absolute/path/server-key.pem:/run/secrets/server_key:ro,Z

Add the mounts under volumes for Compose or as Volume= entries for Quadlet. Restart Team Server, then verify:

curl -f https://team-server.example.com/_readiness

Use -k only while testing a self-signed certificate.

Set Up the First Administrator

An OIDC user must sign in once before the user record exists. Find the user ID:

podman compose -f podman-compose.yaml exec endura-team-server \
  endura task user_get_id email:admin@example.com

For direct Podman, replace the Compose prefix with podman exec endura-team-server.

Assign the Administrator role:

podman compose -f podman-compose.yaml exec endura-team-server \
  endura task user_set_role id:42 role:administrator

Refresh the browser or sign in again.

Operate the Deployment

Logs and Status

podman compose -f podman-compose.yaml ps
podman compose -f podman-compose.yaml logs -f endura-team-server
podman compose -f podman-compose.yaml logs -f postgres

For Quadlet, use systemctl --user status endura-team-server and journalctl --user -u endura-team-server -f.

Update

Review Runtime Sensor compatibility before a major update.

Podman Compose:

podman compose -f podman-compose.yaml pull endura-team-server
podman compose -f podman-compose.yaml up -d endura-team-server

Quadlet:

podman pull ghcr.io/endurasecurity/container/endura-team-server:testing
systemctl --user restart endura-team-server.service

Back Up and Restore

podman compose -f podman-compose.yaml exec -T postgres \
  sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > team_server.sql
tar -czf team_server_config.tar.gz config/

Restore only into an empty or intentionally replaced database:

podman compose -f podman-compose.yaml exec -T postgres \
  sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"' < team_server.sql

Use the database provider’s backup procedure when PostgreSQL is external.

Uninstall

Stop Compose services while preserving their database volume:

podman compose -f podman-compose.yaml down

Remove a direct container or Quadlet service:

systemctl --user disable --now endura-team-server.service
podman rm --force endura-team-server

Deleting the Compose Volume Is Permanent

podman compose -f podman-compose.yaml down --volumes deletes PostgreSQL and all Team Server data. Back up the database and confirm the project directory before running it.

Troubleshooting

Container Does Not Start

podman compose -f podman-compose.yaml config
podman compose -f podman-compose.yaml logs endura-team-server

Check the configuration mount, referenced environment variables, and the two JWT secrets.

Database Connection Fails

podman compose -f podman-compose.yaml logs postgres
podman compose -f podman-compose.yaml exec postgres \
  sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Compose uses the database host postgres. For an external database, confirm DNS, port 5432, TLS, and credentials from the Team Server container.

Rootless Service Stops After Logout

loginctl show-user "$USER" -p Linger
systemctl --user status endura-team-server.service

Enable lingering and use Quadlet for a persistent rootless direct deployment.

SELinux Denies a Mount

Use :Z on private bind mounts and review denials with ausearch -m AVC -ts recent.

Internal Integration URL Is Blocked

Configure settings.http.allowed_networks and, when needed, settings.http.ca_certificates. See Outbound HTTP Policy.

For additional help, contact support@endurasecurity.com.