Running CI tests like production

Table of Contents

I got burned last month: my tests passed in CI but the app crashed in production because of a file permission bug. The tests ran as root, but production runs as a regular user. I want my CI containers to use the same non-root user as production so I catch these issues early.

First I set up my Dagger project and install daggerlib:

dagger init --sdk=python
dagger install github.com/Konubinix/daggerlib

The following is only for testing purposes — it makes the project use the local daggerlib checkout instead of the published version:

sed -i '/pin/d; s|"github.com/Konubinix/daggerlib@main"|"../.."|; s|"\.\./\.\.",|"../.."|' dagger.json

Then I write my Dagger module. Each <<...>> reference is defined in the sections below — they show the function code and its output:

import dagger
from dagger import dag, function, object_type


@object_type
class CiLikeProduction:
    @function
    async def whoami_check(self) -> str:
        """Check that the container runs as a regular user."""
        return await (
            dag.lib().alpine_user()
            .with_exec(['whoami'])
            .stdout()
        )
    @function
    async def home_check(self) -> str:
        """Check the user has a real home directory."""
        return await (
            dag.lib().alpine_user()
            .with_exec(['pwd'])
            .stdout()
        )
    @function
    async def uid_check(self) -> str:
        """Check the user has UID 1000."""
        return await (
            dag.lib().alpine_user()
            .with_exec(['id', 'sam'])
            .stdout()
        )
    @function
    async def debian_uid_check(self) -> str:
        """Check the Debian user has UID 1000."""
        return await (
            dag.lib().debian_user()
            .with_exec(['id', 'sam'])
            .stdout()
        )

1. Making sure the test container runs as a regular user

First I need to verify that my test container actually runs as a non-root user, not root:

@function
async def whoami_check(self) -> str:
    """Check that the container runs as a regular user."""
    return await (
        dag.lib().alpine_user()
        .with_exec(['whoami'])
        .stdout()
    )
dagger call whoami-check

2. The test runner needs a proper home directory

My test harness writes temporary config files to $HOME. I need to make sure the user has a real home directory, not / or /nonexistent:

@function
async def home_check(self) -> str:
    """Check the user has a real home directory."""
    return await (
        dag.lib().alpine_user()
        .with_exec(['pwd'])
        .stdout()
    )
dagger call home-check

3. Matching the production UID

Our Kubernetes pods run with UID 1000. I need the CI user to have the same UID so that volume mounts and file ownership behave identically:

@function
async def uid_check(self) -> str:
    """Check the user has UID 1000."""
    return await (
        dag.lib().alpine_user()
        .with_exec(['id', 'sam'])
        .stdout()
    )
dagger call uid-check

4. Some tests need Debian for native libraries

A few integration tests link against libpq, which is easier on Debian. Same non-root guarantee:

@function
async def debian_uid_check(self) -> str:
    """Check the Debian user has UID 1000."""
    return await (
        dag.lib().debian_user()
        .with_exec(['id', 'sam'])
        .stdout()
    )
dagger call debian-uid-check

Author: root

Created: 2026-04-18 Sat 21:16

Validate