Montscan: an FTP server on by default, two SAMBA prefixes, and a Go version gap
🖨️ Automated scanner document processor with AI-powered naming and WebDav integration. Receives scans via FTP, extracts text using Vision AI, generates intelligent filenames with Ollama AI, and uploads to your cloud storage.
At a glance
- What is it?
- Montscan renames scanned PDFs with an Ollama vision model and delivers them to WebDAV or an SMB share, taking input over FTP, an SMB poll or a watched folder. The configuration tables say as much about what is left out as what is set: an FTP listener on every interface with scanner/scanner123, a published port range that no variable controls, and a stated Go prerequisite that disagrees with go.mod.
- Who is it for?
- Montscan fits a home or small office where one person controls both the scanner and the destination share, and it fits poorly in front of an untrusted network as it ships. FTP answers on 0.0.0.0:21 as scanner/scanner123, WEBDAV_INSECURE turns off certificate verification, and SAMBA_SERVER_DELETE_AFTER_READ removes the file from the source share.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 39 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One SAMBA prefix names both the inbound poller and the outbound provider
Ingress and egress share a prefix. Everything that pulls a document in is named SAMBA_SERVER_: SAMBA_SERVER_ENABLED defaults to false, SAMBA_SERVER_HOST to localhost, SAMBA_SERVER_PORT to 445, SAMBA_SERVER_SHARE to scans, SAMBA_SERVER_PATH to /scans, SAMBA_SERVER_POLL_INTERVAL_SEC to 10. Everything that pushes a finished document out drops the _SERVER_ part: SAMBA_ENABLED defaults to false, SAMBA_HOST to localhost, SAMBA_PORT to 445, SAMBA_SHARE to scans, SAMBA_PATH to scans. There is no shared switch, so turning on SAMBA_ENABLED and turning on SAMBA_SERVER_ENABLED are two separate decisions, yet both default their share name to scans and both want a username and a password you have to supply twice.
The rename shows up in the shipped files. .env.example carries a commented block of SAMBA_INGRESS_ENABLED through SAMBA_INGRESS_WORK_DIR marked as deprecated aliases, and no configuration table names them, so an environment carried forward from an earlier release depends on keys whose current names differ by one word.
One default deserves attention before you point this at a share. SAMBA_SERVER_DELETE_AFTER_READ is true, so the poller removes the file from the remote SMB share once it has copied it into SAMBA_SERVER_WORK_DIR. A wrong SAMBA_SERVER_PATH, or a work directory the container cannot write, leaves the source copy gone and nothing staged.
The FTP ingress binds every interface with scanner and scanner123
FTP is the one ingress nobody has to ask for. FTP_ENABLED defaults to true while FOLDER_ENABLED and SAMBA_SERVER_ENABLED both default to false, and the values that decide who can reach the service are FTP_HOST 0.0.0.0, FTP_PORT 21, FTP_USERNAME scanner, FTP_PASSWORD scanner123, with FTP_UPLOAD_DIR pointing at ./scans. A default install therefore offers an FTP service on every interface of the host, on the privileged port, with a credential pair printed in the project's own files.
The compose file undercuts the obvious remedy. One line sets FTP_USERNAME=scanner as a literal while the next reads FTP_PASSWORD=${FTP_PASSWORD:-scanner123}. The password is wired to your .env and the username is not, so editing FTP_USERNAME there changes nothing under a compose start and you have to edit the compose file itself. .env.example at least replaces both values with REPLACE_ME, and the documented local run applies that file directly:
cp .env.example .env
# edit .env with your values
export $(grep -v '^#' .env | xargs)
./montscanTwo more lines in the same compose service are literals: FTP_UPLOAD_DIR=/app/scans and SAMBA_SERVER_WORK_DIR=/app/scans, even though the tables describe SAMBA_SERVER_WORK_DIR as inheriting FTP_UPLOAD_DIR when it is unset. Change FTP_UPLOAD_DIR and the poller's staging directory stays where it was.
Eleven ports are published with nothing that sets them
The Dockerfile ends with EXPOSE 21 21000-21010, and the compose service publishes 21:21 alongside 21000-21010:21000-21010. That second mapping covers eleven consecutive ports, and no setting in the project chooses them. The FTP table exposes exactly two network options, FTP_HOST and FTP_PORT. No passive port range variable appears in the configuration options or in .env.example, so the range is fixed in the image: it cannot be moved with an environment variable, and a host firewall that opens 21 alone gives you a control channel and no data channel.
The Dockerfile declares no USER, so the listener and the PDF conversion both run as root inside the container, on a port that needs privileges to bind. That is a container detail rather than a host compromise, but it describes the shape of the thing. The FTP server is the front door, it is on by default, and its username and password appear in the README table, the compose service and .env.example in three slightly different forms, which is where a careless deployment picks up the wrong one.
The folder watcher renames originals in place when no output directory is given
The folder watcher is the only ingress that needs neither a listener nor credentials: FOLDER_ENABLED=false, FOLDER_INPUT_DIR=./input, FOLDER_POLL_INTERVAL_SEC=5. Its one real decision is FOLDER_OUTPUT_DIR. Set it and each PDF is renamed with an Ollama generated name and moved there. Omit it and the rename happens where the file already sits, so the original scan name is gone and nothing is copied anywhere else. The project treats that as a supported mode with its own setup block rather than as an edge case:
FOLDER_ENABLED=true
FOLDER_INPUT_DIR=/mnt/scanner # your Samba mount point
FOLDER_OUTPUT_DIR=/mnt/documents # where renamed files land
FOLDER_POLL_INTERVAL_SEC=5
FTP_ENABLED=falseThis is also the arrangement that needs no egress provider at all, matching the note that the watcher can act as both ends of the pipeline. The cost is that rename in place is the only ingress mode with no staging copy, so a bad filename lands on the original file with nothing to fall back to, while the samba ingress keeps a working copy in a separate work directory.
Left at its defaults the mode is inert until you flip FOLDER_ENABLED, and a 5 second poll picks up whatever appears in the input directory within that window. No configuration table offers a settle delay or a file size threshold between the poll and the model call, so a scan still being copied onto the mount is presented to the vision step on the same schedule as a finished one.
Prerequisites say Go 1.24+ while go.mod says go 1.25.0
The stated prerequisite is Go 1.24+. The build file says go 1.25.0 under module Montscan, and the image builds on golang:1.25.5-alpine3.23. A machine on Go 1.24 satisfies the sentence in the prerequisites and fails the toolchain line, so the two disagree in the direction that costs an afternoon.
git clone https://github.com/sh4den/Montscan.git
cd Montscango build -o montscan .Conversion is external, and the container carries only one of the two documented paths. The runtime image installs poppler-utils, ca-certificates and tzdata on alpine:3.23, so a container install gets Poppler. ImageMagick is named in the prerequisites as the alternative converter and appears nowhere in the Dockerfile, which makes it a local install choice rather than a container one. Whichever converter you use, the model has to exist before any file gets named:
# Install Ollama from https://ollama.ai/
ollama pull llava
# or any other vision-capable modelFour direct dependencies, and the FTP server pinned to a 2020 timestamp
go.mod requires four direct packages: github.com/fatih/color v1.18.0 for terminal output, github.com/goftp/server at pseudo-version v0.0.0-20200708154336-f64f7c2d8a42, github.com/hirochachacha/go-smb2 v1.1.0 for both SMB directions, and github.com/studio-b12/gowebdav v0.9.0 for the WebDAV provider. The FTP server, which is the headline ingress and the only ingress enabled by default, sits on a pseudo-version whose timestamp reads 2020-07-08.
Its indirect entries explain themselves: github.com/jlaffaye/ftp v0.2.0 and github.com/geoffgarside/ber v1.1.0 are the client side of goftp's file driver, and github.com/goftp/file-driver is pinned the same way. github.com/stretchr/testify v1.8.3 is listed as indirect although no test file appears among the top-level entries, which are main.go and four directories: agent/, config/, providers/ and server/.
There is no HTTP client in the require list at all. With OLLAMA_HOST at http://localhost:11434 and OLLAMA_MODEL at llava by default, both the vision step and the naming step leave through the standard library, which also means the timeout and retry behaviour of the calls that decide every filename is not something the dependency list will tell you about.
The compose entrypoint pulls the Ollama model on every start
The compose file runs Ollama as a service whose entrypoint pulls the model every time the container comes up. The shared anchor launches /bin/sh -lc, runs ollama serve in the background, captures the pid, echoes a waiting message, sleeps 5 seconds, echoes the model it is about to pull, runs ollama pull against $OLLAMA_MODEL, then waits on the server process. Restart the container and the pull runs again, against ollama/ollama:latest with no version pinned. The montscan service itself runs ghcr.io/sh4den/montscan:latest, with the source build line left commented out beside it.
Which model that pull fetches depends on which file you edited. The configuration table and the compose environment both default to llava, .env.example sets OLLAMA_MODEL=gemma4:26b-a4b-it-qat, and the prerequisites offer llama3.2-vision as an example. Three starting points for one variable, and nothing in the configuration says which of the three the project intends.
Starting the published image is one command:
docker-compose up -dAcceleration is a profile choice rather than a flag: COMPOSE_PROFILES=gpu with OLLAMA_GPU_COUNT=1 in .env, or COMPOSE_PROFILES=cpu, which is what .env.example ships. Without a profile pointed at a GPU the model runs on CPU, and every page in the stack waits for it.
A 97.5 percent figure arrives with no method, and the text ends mid-word
The opening note carries two claims at once: that Montscan is not fully production-ready and is currently in active development but is fairly usable, and that it reaches a 97.5% success rate on a small test set of 1000 documents. The number arrives without a definition. Nothing says what counts as a success, what the other 25 documents were, what mix of pages the set held, or which model produced the filenames being counted. A figure the same sentence calls small describes a sample rather than a deployment, and it belongs to the project rather than to any outside test.
The documentation also stops early. The table of contents promises Prerequisites, Installation, Configuration Options, Usage, Docker Deployment, Troubleshooting and License. The text runs out inside the compose walkthrough, at the line telling you to build from source instead and uncomment the `buil`, with the rest of the word missing. Everything past that point is absent: no build-from-source recipe, no Troubleshooting section, no License section, even though a LICENSE file sits at the top level and MIT is the declared license.
That gap lands exactly where this project's behaviour is least predictable. A tool that renames originals in place when FOLDER_OUTPUT_DIR is unset, removes files from the source share when SAMBA_SERVER_DELETE_AFTER_READ is true, and can skip TLS verification with WEBDAV_INSECURE ships with no documented error handling, retry or rollback section. Those three flags are where a deployment is most likely to lose something, and the missing pages are exactly the ones that would have said what happens when a conversion or a model call fails. The release record gives a reason to keep reading rather than to stop: v1.3.0 on 2026-06-15, v1.3.1 on 2026-07-19 and v1.3.2 on 2026-08-24, the last of them the same day as the most recent push, with the repository not archived.
Editorial conclusion
Montscan fits a home or small office where one person controls both the scanner and the destination share, and it fits poorly in front of an untrusted network as it ships. FTP answers on 0.0.0.0:21 as scanner/scanner123, WEBDAV_INSECURE turns off certificate verification, and SAMBA_SERVER_DELETE_AFTER_READ removes the file from the source share. Before you trust a run, change the FTP password, bind FTP_HOST to one interface, and confirm you have a Go 1.25 toolchain rather than the 1.24 the prerequisites name. The 97.5 percent figure covers 1000 documents in a test the project itself calls small and gives no method for, so measure your own stack before moving anything you would not want to lose.
Frequently asked questions
Does Montscan's folder watcher mode need any network port?
No. FOLDER_ENABLED defaults to false, FOLDER_INPUT_DIR defaults to ./input and FOLDER_POLL_INTERVAL_SEC defaults to 5. FOLDER_OUTPUT_DIR is the only one of the three that changes where a renamed PDF ends up, and leaving it unset renames the file in the input directory.
What does Montscan use as FTP credentials out of the box?
FTP_USERNAME defaults to scanner and FTP_PASSWORD to scanner123, with FTP_HOST at 0.0.0.0 and FTP_PORT at 21. Under docker compose the username is a literal in the service while the password comes from FTP_PASSWORD with scanner123 as its fallback.
Which Go version does building Montscan actually need?
go.mod declares go 1.25.0 and the Dockerfile builds on golang:1.25.5-alpine3.23, while the stated prerequisite is Go 1.24+. The toolchain line in go.mod is the one that has to hold for a local build.
What model does Montscan use to read pages and name files?
OLLAMA_MODEL defaults to llava and OLLAMA_HOST to http://localhost:11434, and the compose entrypoint pulls whatever OLLAMA_MODEL names at every container start. .env.example sets gemma4:26b-a4b-it-qat instead, so the shipped example and the documented default are different models.
Does Montscan keep a copy of the original scan?
That depends on the ingress. The samba poller copies a file into SAMBA_SERVER_WORK_DIR and then removes it from the share when SAMBA_SERVER_DELETE_AFTER_READ is true, while the folder watcher with FOLDER_OUTPUT_DIR unset renames the PDF where it already sits and keeps no second copy.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sh4den-montscan)