# ipa-server: uploading and installing .ipa and .apk builds from the browser

> ipa-server is a Go service that accepts Apple .ipa and Android .apk uploads over HTTP, parses the package, extracts an icon and hands back an install link. It suits in-house distribution teams that want a self-hosted, single-binary release portal, but it ships no TLS certificate of its own and still expects a reverse proxy in front of it.

**iineva/ipa-server** — Upload and install iOS ipa and Android apk in web.

- Repository: https://github.com/iineva/ipa-server
- Website: https://ipa-server-git-v2-ineva.vercel.app/
- Stars: 798 · Forks: 257
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/iineva-ipa-server

## A browser upload screen instead of an over-the-air install link

ipa-server is a single Go service for a narrow job: accepting an Apple .ipa or an Android .apk file that someone has already built and signed, then making that file installable from a web page. The project describes itself as "Upload and install Apple `.ipa` and Android `.apk` in web", and its topic tags name the intended use case directly: ad-hoc, in-house, deploy, iphone, ipad, android, apk, ipa.

That framing matters. This is not an app store, not a release pipeline and not a code signing service. It has no concept of a tester roster, no per-device UDID registry and no build pipeline. It assumes the binary handed to it is already signed the way the target organisation expects, and it removes only the last mile of delivery: getting a signed package onto a phone without mailing someone a file.

The audience is therefore a team already doing ad-hoc or in-house distribution who has outgrown sending links by hand. The service gives you a hosted upload page, a list of what has been uploaded, and links that a phone can open. Anything beyond that, such as telling which devices are allowed to install a build, is outside its scope.

The codebase is small and idiomatic for a self-hosted web tool. The repository has cmd/, pkg/ and public/ directories alongside a Dockerfile, a Makefile, docker-compose.yml, heroku.yml and app.json, which points to a web server with a static front end rather than a library meant to be imported into another product.

## Package parsing and icon extraction handled inside the Go binary

Once a file arrives, the server inspects it rather than serving it blind. The feature list promises automatic parse of packet information, automatic icon generation, and parsing of icons from `Assets.car`, the compiled asset catalogue inside an iOS app bundle. The dependency list supports that description without needing to be read further into the code: `howett.net/plist` for property list parsing, `github.com/iineva/bom` for Apple BOM files, `github.com/shogo82148/androidbinary` for Android binary inspection, and `github.com/poolqa/CgbiPngFix` for correcting PNG colour data that iOS asset pipelines mangle.

The stated deployment model is one static binary with everything needed inside it: "A single binary build-in all you need". The Makefile confirms that shape, since the web target is a single `go run` of one entrypoint file and the build target produces one executable, with no asset pipeline or separate worker process in the same repository.

Storage is where the generated files go. The docs say generated files are completely stored in external storage, currently supporting `S3`, `Qiniu` and `Alibaba Cloud OSS`, with matching SDKs in go.mod for each: the AWS SDK v2 with its S3 service client, `github.com/qiniu/go-sdk/v7`, and `github.com/aliyun/aliyun-oss-go-sdk`. When no remote storage is configured, the container still exposes a local volume at /app/upload, so a single-node setup is possible without a bucket, but the design is clearly built around object storage plus a CDN front door.

## Configuring the remote storage URL scheme and the metadata path

Configuration is entirely environment driven, which suits container platforms that inject variables rather than files. Four settings carry most of the behaviour.

`PUBLIC_URL` sets the public address of the server and falls back to `$DOMAIN` when left empty, which is convenient on Heroku. `REMOTE` takes a single URI-shaped string whose scheme selects the backend and whose remaining segments carry the endpoint, the access key, the secret key and the bucket. The accepted forms are `s3://ENDPOINT:AK:SK:BUCKET`, `alioss://ENDPOINT:AK:SK:BUCKET` and `qiniu://[ZONE]:AK:SK:BUCKET`. Note that the credentials travel inside one environment variable, separated by colons, which means the secret key is part of a single string that ends up in process environments and container inspection output. That is worth thinking about for any team with stricter secret handling, and nothing here documents an alternative such as a mounted credentials file.

`REMOTE_URL` is the public base URL of the storage, given as something like `https://cdn.example.com`, so uploaded packages are downloaded from the CDN rather than from the Go process. `META_PATH` chooses where the app list is written, defaulting to `appList.json`, and the compose file comments recommend a random secret path so metadata is not trivially guessable if remote storage is in use. The compose example also carries `META_DATA_FILTER` for a comma separated list of metadata keys, `UPLOAD_DISABLED` to close uploads after the first push, and `LOGIN_USER` and `LOGIN_PASS` to require credentials for upload and delete.

The environment block in the published compose example reads like this:

```yaml
version: "3"
services:
  web:
    image: ineva/ipa-server:latest
    container_name: ipa-server
    restart: unless-stopped
    environment:
      # server public url
      - PUBLIC_URL=https://<YOUR_DOMAIN>
      # option, remote storager config, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET
      - REMOTE=
```

Every placeholder there is yours to replace, and the REMOTE line stays empty unless you are wiring up object storage.

## Running it locally with docker-compose and opening port 9008

The quickest path to a working instance is the compose file, which builds the image from the repository, exposes the service and mounts a host directory for metadata. The described path is a local trial, and the compose file maps host port 9008 to container port 8080.

```shell
# clone
git clone https://github.com/iineva/ipa-server
# build and start
cd ipa-server
docker-compose up -d
# than open http://localhost:9008 in your browser.
```

After that the browser opens http://localhost:9008. The compose service itself sets `PUBLIC_URL=` empty, `REMOTE=` and `REMOTE_URL=` empty, `META_PATH=appList.json`, `DELETE_ENABLED=false`, and mounts `/docker/data/ipa-server` at `/app/upload`. The container is built with `restart: unless-stopped`, so a restart of the host brings the portal back without a manual start.

The first real use is straightforward once the page loads: upload a signed .ipa or .apk, let the service parse it and generate the icon, then open the resulting link on a device. The screenshot pair in the docs shows a home view and a detail view, which suggests the entry point is a browsable list rather than an upload-only form. Anyone who wants to confirm the parse worked on a real package should check that the detail page shows a correctly rendered icon, since an icon pulled from `Assets.car` is a common failure point in oddly repackaged builds.

Building from source is the alternative path. Go has to be installed first, then the Makefile does the work:

```shell
# install golang v1.16 first
git clone https://github.com/iineva/ipa-server
# build and start
cd ipa-server
# build binary
make build
# run local server
make
```

The `make` target runs the binary with a `-del` flag, a `debug` target adds `-d`, and a `test` target runs `go test ./...`.

## No TLS certificate ships with the container, so a reverse proxy is required

The clearest constraint in the documentation concerns transport security: "This server is not included SSL certificate. It must run behide the reverse proxy with HTTPS." The typo is in the original, and the meaning is not negotiable. Out of the box the service speaks plain HTTP, so publishing it directly would expose both the upload endpoint and the download links without encryption.

The project ships a concrete remedy rather than leaving it to the reader. The production compose example adds a second service running Caddy, which terminates TLS on ports 80 and 443 and proxies to the Go container:

```yaml
  caddy:
    image: ineva/caddy:2.4.1
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    entrypoint: |
      sh -c 'echo "$$CADDY_CONFIG" > /srv/Caddyfile && /usr/bin/caddy run --config /srv/Caddyfile'
    environment:
      CADDY_CONFIG: |
        <YOUR_DOMAIN> {
          reverse_proxy web:8080
        }
```

Two details in that file carry operational weight. `UPLOAD_DISABLED="true"` appears in the production example but not in the local trial file, which describes the intended workflow: upload builds during a release window, then close the door so the portal becomes a read-only distribution page. And `LOGIN_USER` and `LOGIN_PASS` are the only access control on offer, guarding upload and delete. With both empty, anyone who can reach the site can upload and, if `DELETE_ENABLED` is true, remove a build. There is no user model, no per-package token and no record of who uploaded what.

A second limit deserves plain statement: `DELETE_ENABLED=false` by default, and nothing in the project documents a soft delete, an undo or a recycle bin. Once a package is removed from the bucket, restoring it is a matter of your own backups.

## Go 1.16 in go.mod against a Go 1.22.6 builder image

The dependency and toolchain story has a wrinkle that a careful adopter will notice. go.mod declares `go 1.16`, and the source build instructions ask for Go v1.16 to be installed first. The Dockerfile, however, builds with `FROM golang:1.22.6 AS builder`, and the runtime image is `ineva/alpine:3.10.3`, a custom base rather than a stock Alpine tag.

The consequence is that the declared language floor and the actual build environment disagree. The go.mod requirement is the lower bound the code claims compatibility with, and building the module with a newer toolchain is normal practice, but the pinned Alpine 3.10.3 runtime is old enough to raise its own questions when you audit the process it runs under. The docs do not explain the split, so treat the go.mod directive as a floor rather than a tested matrix.

The dependency set is worth a look before you commit. `github.com/gorilla/websocket` is present, which suggests some live progress reporting while a large package is parsed, and `github.com/go-kit/kit` points at the standard logging and transport toolkit. `github.com/spf13/afero` gives a filesystem abstraction, consistent with the design where local disk and object storage are interchangeable backends.

The published image tag is `ineva/ipa-server:latest` in the compose example, while the Makefile builds `ineva/ipa-server` with a version variable set to `2.5.5`. The tagged releases listed for the project run higher, at v2.7.0 and v2.7.1 in July 2025. That gap suggests the Makefile constant is not the release version, so pinning by tag rather than by `latest` is the safer habit if you want reproducible deployments.

## A narrow alternative to a shared folder or a full OTA platform

The honest comparison is not against a competing open source project but against the two dull options most teams already have: a shared network folder or a general purpose app distribution platform.

Against a shared folder, ipa-server wins on delivery and loses on everything else. A folder gives you zero setup and no server to patch, but every device must reach the folder over the network, iOS installation from a folder requires manual steps through the Files app, and nothing in a folder renders an app icon or shows a version list. ipa-server turns the same binary into a page with a parse and a link, and it stores the file on object storage, so the phone never needs access to an internal network. The trade is that you now operate a service, its storage bucket and its proxy.

Against a managed over-the-air update platform, the difference in approach is granularity. A full OTA platform is built around applications that pull updates: the device registers itself, checks a version endpoint and installs when a newer build is published. ipa-server has no device model at all. It serves a static list of uploaded packages, and the human on the phone decides which link to open. That is the right shape for an in-house build distributed to a known team of colleagues, and the wrong shape for anything that needs per-device rollout control or entitlement checks.

The last difference worth naming is operational cost. ipa-server is one Go binary and a compose file, with no database server in the picture: metadata is a JSON file at `META_PATH`, and the volume mount at `/app/upload` is the only persistence the compose example requires. A list of packages in one JSON file is simple and easy to back up, but it also means the deployment does not scale horizontally without deciding how that file is shared.

## Maintenance state and the upgrade cost of a v2 service

The repository is not archived. Its last push was on 2026-04-08, and the most recent tagged releases are v2.7.1, v2.7.0 and v2.6.1, all published on 2025-07-22 within about two hours of each other. That pattern looks like a concentrated maintenance session rather than a steady release cadence, and it is the sort of thing to ask about directly before depending on it.

The v2 line is a rewrite of the earlier code. The project states that ipa-server is updated to v2 and links the older version v1 on a separate branch, so anyone running the v1 deployment is looking at a different program with a different feature set rather than a version bump. The v2.7.0 note mentions optimisation of human readable file names, and v2.7.1 carries only a bug fixed note, so the recent changes have been cosmetic and corrective rather than structural.

Upgrade cost is therefore low in code terms, since the whole service is one binary behind an environment configuration, and the configuration keys look stable across the listed releases. The practical upgrade cost sits elsewhere: a new release means a new container image, a check that your `REMOTE` string still matches the expected scheme format, and a look at whether anything you relied on through `META_DATA_FILTER` still behaves the same. The licence is MIT, so redistribution and internal modification carry no copyleft obligation, though that is a statement about licence terms rather than advice.

One operational habit is worth building in from the start, and it comes from the project's own configuration rather than generic practice: keep `META_PATH` on the mounted volume and snapshot that volume together with the storage bucket, since the service keeps no other record of what has been published.

## Conclusion

Adopt ipa-server if you need a small, self-hosted in-house distribution portal and can put an HTTPS reverse proxy in front of it and accept a Go 1.16 era dependency set. Do not adopt it if your distribution needs App Store signing flows, per-user entitlements or device management, and check first that your storage bucket is reachable from the container and that META_PATH points at a persistent volume, because the container mounts /app/upload for the uploaded packages themselves.

## FAQ

### What does ipa-server do and who is it for?

It is a Go service that accepts Apple .ipa and Android .apk uploads in the browser, parses the package information, generates an icon and serves an install link. It is framed for ad-hoc and in-house distribution of builds that are already signed.

### How do I run ipa-server locally for a first test?

Clone the repository, then run `docker-compose up -d` in the project directory and open http://localhost:9008. The compose file builds the image, exposes container port 8080 on host port 9008 and mounts `/docker/data/ipa-server` at `/app/upload`.

### Does ipa-server serve HTTPS on its own?

No. The server does not include an SSL certificate and must run behind a reverse proxy with HTTPS. The published compose example adds a Caddy container that terminates TLS on ports 80 and 443 and proxies to `web:8080`.

### Where are the uploaded ipa and apk files stored?

Generated files are stored in external storage, currently supporting S3, Qiniu and Alibaba Cloud OSS. The `REMOTE` variable selects the backend with a string such as `s3://ENDPOINT:AK:SK:BUCKET`, and `REMOTE_URL` gives the public CDN address used for downloads.

### Can I restrict who uploads or deletes apps?

There is a single optional credential pair, `LOGIN_USER` and `LOGIN_PASS`, that guards uploading and deleting apps. The project describes no user roles, and no audit trail of who performed an upload is documented.

### Is ipa-server still being worked on?

The repository is not archived and the last push was on 2026-04-08. The listed releases v2.7.0 and v2.7.1 both carry July 2025 dates, with notes about human readable file names and a bug fix.

## Sources

- [iineva/ipa-server on GitHub](https://github.com/iineva/ipa-server)
- [License: MIT](https://github.com/iineva/ipa-server/blob/main/LICENSE)
- [Project website](https://ipa-server-git-v2-ineva.vercel.app/)
- [README](https://github.com/iineva/ipa-server/blob/main/README.md)
- [Releases](https://github.com/iineva/ipa-server/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/iineva-ipa-server
