├── .gitignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── dependabot-auto.yml │ └── publish.yml ├── test.sh ├── .editorconfig ├── _config.yml ├── Dockerfile ├── src ├── docker-entrypoint.sh └── vsftpd.conf ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @garethflowers 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://paypal.me/garethflowers" 2 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | export IMAGE_NAME=garethflowers/ftp-server 4 | 5 | docker build --tag $IMAGE_NAME . 6 | docker run --rm $IMAGE_NAME sh -c 'vsftpd -version 0>&1' 7 | 8 | echo "\nOK" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | docker_repository: garethflowers/ftp-server 2 | author: garethflowers 3 | plugins: 4 | - jekyll-remote-theme 5 | - jekyll-sitemap 6 | remote_theme: garethflowers/garethflowers.github.io 7 | tagline: Gareth Flowers 8 | twitter: 9 | username: garethflowers 10 | card: summary 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "docker" 8 | commit-message: 9 | include: "scope" 10 | prefix: "chore" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22.1 2 | ENV FTP_USER=foo \ 3 | FTP_PASS=bar \ 4 | GID=1000 \ 5 | UID=1000 \ 6 | PUBLIC_IP=0.0.0.0 7 | 8 | RUN apk add --no-cache --update \ 9 | vsftpd==3.0.5-r2 10 | 11 | COPY [ "/src/vsftpd.conf", "/etc" ] 12 | COPY [ "/src/docker-entrypoint.sh", "/" ] 13 | 14 | ENTRYPOINT [ "/docker-entrypoint.sh" ] 15 | CMD [ "/usr/sbin/vsftpd" ] 16 | EXPOSE 20/tcp 21/tcp 40000-40009/tcp 17 | HEALTHCHECK CMD netstat -lnt | grep :21 || exit 1 18 | -------------------------------------------------------------------------------- /src/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FTP_DATA=/home/$FTP_USER 3 | FTP_GROUP=$( getent group "$GID" | awk -F ':' '{print $1}' ) 4 | 5 | if test -z "$FTP_GROUP"; then 6 | FTP_GROUP=$FTP_USER 7 | addgroup -g "$GID" -S "$FTP_GROUP" 8 | fi 9 | 10 | adduser -D -G "$FTP_GROUP" -h "$FTP_DATA" -s "/bin/false" -u "$UID" "$FTP_USER" 11 | 12 | mkdir -p $FTP_DATA 13 | chown -R "$FTP_USER:$FTP_GROUP" "$FTP_DATA" 14 | echo "$FTP_USER:$FTP_PASS" | /usr/sbin/chpasswd 15 | 16 | sed -i -r "s/0.0.0.0/$PUBLIC_IP/g" /etc/vsftpd.conf 17 | 18 | touch /var/log/vsftpd.log 19 | tail -f /var/log/vsftpd.log | tee /dev/stdout & 20 | touch /var/log/xferlog 21 | tail -f /var/log/xferlog | tee /dev/stdout & 22 | 23 | if [ "$*" = "/usr/sbin/vsftpd" ]; then 24 | /usr/sbin/vsftpd 25 | else 26 | exec "$@" 27 | fi 28 | -------------------------------------------------------------------------------- /src/vsftpd.conf: -------------------------------------------------------------------------------- 1 | # daemon 2 | background=NO 3 | listen_ipv6=NO 4 | listen=YES 5 | session_support=NO 6 | 7 | # access 8 | anonymous_enable=NO 9 | ftpd_banner=FTP Server 10 | local_enable=YES 11 | 12 | # local user 13 | allow_writeable_chroot=YES 14 | chroot_local_user=YES 15 | guest_enable=NO 16 | local_umask=022 17 | passwd_chroot_enable=YES 18 | 19 | # local time 20 | use_localtime=YES 21 | 22 | # directory 23 | dirlist_enable=YES 24 | dirmessage_enable=NO 25 | hide_ids=YES 26 | 27 | # file transfer 28 | write_enable=YES 29 | 30 | # logging 31 | dual_log_enable=YES 32 | log_ftp_protocol=YES 33 | xferlog_enable=YES 34 | 35 | # network 36 | connect_from_port_20=NO 37 | ftp_data_port=20 38 | max_clients=0 39 | max_per_ip=0 40 | pasv_address=0.0.0.0 41 | pasv_addr_resolve=YES 42 | pasv_promiscuous=YES 43 | pasv_enable=YES 44 | pasv_max_port=40009 45 | pasv_min_port=40000 46 | port_enable=YES 47 | 48 | # tweaks 49 | seccomp_sandbox=NO 50 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot PR Approve and Merge 2 | on: pull_request_target 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | jobs: 7 | dependabot: 8 | runs-on: ubuntu-latest 9 | if: ${{ github.actor == 'dependabot[bot]' }} 10 | steps: 11 | - name: Dependabot Metadata 12 | id: dependabot-metadata 13 | uses: dependabot/fetch-metadata@v2.4.0 14 | with: 15 | github-token: "${{ secrets.GITHUB_TOKEN }}" 16 | - name: Approve a PR 17 | run: gh pr review --approve "$PR_URL" 18 | env: 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | - name: Enable auto-merge for Dependabot PRs 22 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 23 | run: gh pr merge --auto --squash "$PR_URL" 24 | env: 25 | PR_URL: ${{ github.event.pull_request.html_url }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gareth Flowers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Images 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - "*.*.*" 11 | jobs: 12 | publish: 13 | name: Publish 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Prepare 19 | id: prep 20 | uses: docker/metadata-action@v5.9.0 21 | with: 22 | images: | 23 | ${{ github.repository_owner }}/ftp-server 24 | ghcr.io/${{ github.repository_owner }}/ftp-server 25 | tags: | 26 | type=edge,branch=main 27 | type=semver,pattern={{version}} 28 | type=semver,pattern={{major}}.{{minor}} 29 | type=semver,pattern={{major}} 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | - name: Login to DockerHub 35 | if: github.event_name != 'pull_request' 36 | uses: docker/login-action@v3 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | - name: Login to GHCR 41 | if: github.event_name != 'pull_request' 42 | uses: docker/login-action@v3 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.repository_owner }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | - name: Build and Push 48 | uses: docker/build-push-action@v6 49 | with: 50 | context: . 51 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x 52 | push: ${{ github.event_name != 'pull_request' }} 53 | tags: ${{ steps.prep.outputs.tags }} 54 | labels: ${{ steps.prep.outputs.labels }} 55 | - name: Update Description 56 | continue-on-error: true 57 | uses: peter-evans/dockerhub-description@v4 58 | with: 59 | password: ${{ secrets.DOCKERHUB_TOKEN }} 60 | repository: ${{ github.repository_owner }}/ftp-server 61 | short-description: ${{ github.event.repository.description }} 62 | username: ${{ secrets.DOCKERHUB_USERNAME }} 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FTP Server 2 | 3 | A simple FTP server, using 4 | [`vsftpd`](https://security.appspot.com/vsftpd.html). 5 | 6 | ## How to use this image 7 | 8 | ### Start a FTP Server instance 9 | 10 | To start a container, with data stored in `/data` on the host, use the 11 | following: 12 | 13 | #### ... via `docker run` 14 | 15 | ```sh 16 | docker run \ 17 | --detach \ 18 | --env FTP_PASS=123 \ 19 | --env FTP_USER=user \ 20 | --env PUBLIC_IP=192.168.0.1 \ 21 | --name my-ftp-server \ 22 | --publish 20-21:20-21/tcp \ 23 | --publish 40000-40009:40000-40009/tcp \ 24 | --volume /data:/home/user \ 25 | garethflowers/ftp-server 26 | ``` 27 | 28 | #### ... via `docker compose` 29 | 30 | ```yml 31 | services: 32 | ftp-server: 33 | container_name: my-ftp-server 34 | environment: 35 | - PUBLIC_IP=192.168.0.1 36 | - FTP_PASS=123 37 | - FTP_USER=user 38 | image: garethflowers/ftp-server 39 | ports: 40 | - "20-21:20-21/tcp" 41 | - "40000-40009:40000-40009/tcp" # For passive mode 42 | volumes: 43 | - "/data:/home/user" 44 | ``` 45 | 46 | ## Configuration 47 | 48 | ### Ports 49 | 50 | | Port | Required? | Description | Config | 51 | | -------------- | --------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | 52 | | 21 | Required | The default port that FTP listens on. | [listen_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) | 53 | | 20 | Required | The default port for PORT connections to originate. | [ftp_data_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) | 54 | | 40000
40009 | Optional | The min and max ports to use for PASV connections to originate. | [pasv_min_port](https://security.appspot.com/vsftpd/vsftpd_conf.html)
[pasv_max_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) | 55 | 56 | ### Environment Variables 57 | 58 | | Variable | Default Value | Description | 59 | | ----------- | ------------- | ------------------------------------------------- | 60 | | `FTP_PASS` | `bar` | The FTP password | 61 | | `FTP_USER` | `foo` | The FTP username | 62 | | `UID` | `1000` | User ID for the `$FTP_USER` user | 63 | | `GID` | `1000` | Group ID for the `$FTP_USER` user | 64 | | `PUBLIC_IP` | `0.0.0.0` | Public IP address to use for Passive connections. | 65 | 66 | ## License 67 | 68 | - This image is released under the 69 | [MIT License](https://raw.githubusercontent.com/garethflowers/docker-ftp-server/master/LICENSE). 70 | --------------------------------------------------------------------------------